/*
 * Copyright (c) zhg2yqq.com Corp.
 * All Rights Reserved.
 */
package com.zhg2yqq.wheels.security.multi;

import java.util.Collection;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;

import com.zhg2yqq.wheels.security.ISecurityService;

import cn.hutool.core.collection.CollUtil;

/**
 * 登录身份处理
 *
 * @author zhg2yqq, 2023年3月16日
 * @version zhg2yqq v1.0
 */
public class CustomAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    protected final Log logger = LogFactory.getLog(getClass());

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    private boolean forcePrincipalAsString = false;

    protected boolean hideUserNotFoundExceptions = true;

    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();

    private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    private UserDetailsService userDetailsService;
    private TokenCheckHelper checkHelper;
    private ISecurityService securityService;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService,
                                        TokenCheckHelper checkHelper) {
        this.userDetailsService = userDetailsService;
        this.checkHelper = checkHelper;
    }

    protected void prevAuthenticationChecks(CustomAuthenticationToken authentication)
            throws AuthenticationException {
        if (!allow(authentication)) {
            throw new AuthenticationServiceException(
                    "Authentication type not supported: " + authentication.getType());
        }
        authentication.prevAuthenticationCheck(checkHelper);
    }

    protected UserDetails retrieveUser(CustomAuthenticationToken authentication)
            throws AuthenticationException {
        return authentication.retrieveUser(userDetailsService);
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  CustomAuthenticationToken authentication)
            throws AuthenticationException {
        authentication.postAuthenticationCheck(userDetails, checkHelper);
    }

    @Override
    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsService, "A userDetailsService must be set");
        Assert.notNull(this.checkHelper, "A checkHelper must be set");
        Assert.notNull(this.messages, "A message source must be set");
        doAfterPropertiesSet();
    }

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        if (!supports(authentication.getClass())) {
            this.logger.info("Only CustomAuthenticationToken is supported");
            return null;
        }

        CustomAuthenticationToken token = (CustomAuthenticationToken) authentication;
        prevAuthenticationChecks(token);

        UserDetails user;
        try {
            user = retrieveUser((CustomAuthenticationToken) authentication);
        } catch (UsernameNotFoundException ex) {
            this.logger.debug("Failed to find user '" + authentication.getName() + "'");
            if (!this.hideUserNotFoundExceptions) {
                throw ex;
            }
            throw new BadCredentialsException(this.messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        try {
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (CustomAuthenticationToken) authentication);
        } catch (AuthenticationException ex) {
            user = retrieveUser((CustomAuthenticationToken) authentication);
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (CustomAuthenticationToken) authentication);
        }
        this.postAuthenticationChecks.check(user);
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    protected Authentication createSuccessAuthentication(Object principal,
                                                         Authentication authentication,
                                                         UserDetails user) {
        UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken
                .authenticated(principal, authentication.getCredentials(),
                        this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        this.logger.debug("Authenticated user");
        return result;
    }

    protected void doAfterPropertiesSet() throws Exception {
    }

    public boolean isForcePrincipalAsString() {
        return this.forcePrincipalAsString;
    }

    public boolean isHideUserNotFoundExceptions() {
        return this.hideUserNotFoundExceptions;
    }

    public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
        this.forcePrincipalAsString = forcePrincipalAsString;
    }

    public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
        this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    protected UserDetailsChecker getPreAuthenticationChecks() {
        return this.preAuthenticationChecks;
    }

    public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
        this.preAuthenticationChecks = preAuthenticationChecks;
    }

    protected UserDetailsChecker getPostAuthenticationChecks() {
        return this.postAuthenticationChecks;
    }

    public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
        this.postAuthenticationChecks = postAuthenticationChecks;
    }

    public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
        this.authoritiesMapper = authoritiesMapper;
    }

    public ISecurityService getSecurityService() {
        return securityService;
    }

    public void setSecurityService(ISecurityService securityService) {
        this.securityService = securityService;
    }

    private boolean allow(CustomAuthenticationToken token) {
        Collection<String> allowTypes = securityService.allowTypes();
        if (CollUtil.isEmpty(allowTypes)) {
            return true;
        }
        return allowTypes.contains(token.getType());
    }

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                CustomAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is locked");
                throw new LockedException(CustomAuthenticationProvider.this.messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"));
            }
            if (!user.isEnabled()) {
                CustomAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is disabled");
                throw new DisabledException(CustomAuthenticationProvider.this.messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            }
            if (!user.isAccountNonExpired()) {
                CustomAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account has expired");
                throw new AccountExpiredException(CustomAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.expired",
                                "User account has expired"));
            }
        }

    }

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                CustomAuthenticationProvider.this.logger.debug(
                        "Failed to authenticate since user account credentials have expired");
                throw new CredentialsExpiredException(CustomAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                                "User credentials have expired"));
            }
        }

    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
